Video Thumbnail
3:32
2:31
clock icon Created with Sketch. 3 minutes

Solution: Mixins

Although in the video I’m using Pydantic V1, we’ve updated the code example to Pydantic V2. As a result, there might be minor differences between the code you see in the video and the code in the Git repository. Notably, .dict() has been replaced by .model_dump().


Jakub Parcheta

I have a question regarding to MRO (children before parents, left-to-right ordering etc.). Using as an example Knight(PowerUp, GameCharacter). To my knowledge first there should go child class atribute (shiled), then PowerUp's damage and finally GameCharacter's name, hp, level. However in the code it goes Knight("Sir Foo", 1000, 10, 100, 100) and it works perfectly, why is that?

REPLY
Arjan Egges

Hi Jakub, in the case of inheritance, superclasses are instantiated before subclasses. So that means that first, the GameCharacter object is instantiated, then PowerUp, and then finally Knight. As a result, the order of the arguments as written in the code works fine. I do not recommend to rely on it in your code too much like in this example, precisely because it leads to a lot of confusion.

REPLY
Dominik Zurek

Hi! I have taken a different approach by creating two protocols for Offensive and Defensive abilities:
defence.py:
from typing import Protocol
from dataclasses import dataclass

class DefensiveAbility(Protocol):
def mitigate_damage(self, damage: int) -> int:
...

@dataclass
class NoBlock:
def mitigate_damage(self, damage: int) -> int:
return damage

@dataclass
class ShieldBlock:
shield: int

def mitigate_damage(self, damage: int) -> int:
damage = max(damage - self.shield, 0)
return damage

offence.py:
from typing import Protocol
from dataclasses import dataclass

class OffensiveAbility(Protocol):
def compute_damage(self, level: int) -> int:
...

@dataclass
class PowerUp:
damage: int

def compute_damage(self, level: int) -> int:
return level * 10 + self.damage

@dataclass
class ManaPool:
mana: int

@dataclass
class Spell:
damage: int
mana_cost: int
mana_pool: ManaPool

def compute_damage(self, level: int) -> int:
if self.mana_pool.mana < self.mana_cost:
return level * 100
else:
self.mana_pool.mana -= self.mana_cost
return level * 10 + self.damage
They are then composed into the GameCharacter class as follows in the character.py file:
from __future__ import annotations
from dataclasses import dataclass
from .offence import OffensiveAbility
from .defence import DefensiveAbility

@dataclass
class GameCharacter:
name: str
hp: int
level: int

offensive_ability: OffensiveAbility
defensive_ability: DefensiveAbility

def compute_damage(self) -> int:
return self.offensive_ability.compute_damage(self.level)

def take_damage(self, damage: int) -> int:
damage = self.defensive_ability.mitigate_damage(damage)
self.hp -= damage
return damage

def attack(self, target: GameCharacter) -> None:
damage = self.compute_damage()
damage_taken = target.take_damage(damage)
print(f"{self.name} attacks {target.name} for {damage_taken} damage!")
if target.hp <= 0:
print(f"{target.name} has been defeated!")
and finally I'm stiching everything together int he main file:
from __future__ import annotations
from .character import GameCharacter
from .offence import PowerUp, ManaPool, Spell
from .defence import ShieldBlock, NoBlock

def main() -> None:
knight = GameCharacter(
name="Sir Foo",
hp=1000,
level=10,
offensive_ability=PowerUp(damage=100),
defensive_ability=ShieldBlock(shield=100),
)
wizard = GameCharacter(
name="Archibald",
hp=1000,
level=5,
offensive_ability=Spell(
damage=100, mana_cost=100, mana_pool=ManaPool(mana=100)
),
defensive_ability=NoBlock(),
)
knight.attack(wizard)
wizard.attack(knight)
knight.attack(wizard)
wizard.attack(knight)
knight.attack(wizard)
wizard.attack(knight)
knight.attack(wizard)
wizard.attack(knight)

if __name__ == "__main__":
main()

I was aiming to keep the GameCharacter class as general as possible, since not every character might need a mana field. That's why I have created the ManaPool class (so it can also be reused for multiple mana abilities that a character might have) that keeps track of the remaining mana of a given character. I'm not sure whether or not it's a good design decision. Thanks in advance for any feedback!

REPLY
Arjan Egges

Nice solution, thanks for sharing!

REPLY